--[[
Name: LibRangeCheck-2.0
Author(s): mitch0
Website: http://www.wowace.com/projects/librangecheck-2-0/
Description: A range checking library based on interact distances and spell ranges
Dependencies: LibStub
License: Public Domain
]]

--- LibRangeCheck-2.0 provides an easy way to check for ranges and get suitable range checking functions for specific ranges.\\
-- The checkers use spell and item range checks, or interact based checks for special units where those two cannot be used.\\
-- The lib handles the refreshing of checker lists in case talents / spells change and in some special cases when equipment changes (for example some of the mage pvp gloves change the range of the Fire Blast spell), and also handles the caching of items used for item-based range checks.\\
-- A callback is provided for those interested in checker changes.
-- @usage
-- local rc = LibStub("LibRangeCheck-2.0")
--
-- rc.RegisterCallback(self, rc.CHECKERS_CHANGED, function() print("need to refresh my stored checkers") end)
--
-- local minRange, maxRange = rc:GetRange('target')
-- if not minRange then
--     print("cannot get range estimate for target")
-- elseif not maxRange then
--     print("target is over " .. minRange .. " yards")
-- else
--     print("target is between " .. minRange .. " and " .. maxRange .. " yards")
-- end
--
-- local meleeChecker = rc:GetFriendMaxChecker(rc.MeleeRange) or rc:GetFriendMinChecker(rc.MeleeRange) -- use the closest checker (MinChecker) if no valid Melee checker is found
-- for i = 1, 4 do
--     -- TODO: check if unit is valid, etc
--     if meleeChecker("party" .. i) then
--         print("Party member " .. i .. " is in Melee range")
--     end
-- end
--
-- local safeDistanceChecker = rc:GetHarmMinChecker(30)
-- -- negate the result of the checker!
-- local isSafelyAway = not safeDistanceChecker('target')
--
-- @class file
-- @name LibRangeCheck-2.0
local MAJOR_VERSION = "LibRangeCheck-2.0"
local MINOR_VERSION = 10000000000003 -- mistakes were made...

local lib, oldminor = LibStub:NewLibrary(MAJOR_VERSION, MINOR_VERSION)
if not lib then
  return
end

local IsClassicVanilla = (_G.WOW_PROJECT_ID == _G.WOW_PROJECT_CLASSIC)
local IsClassicBC = (_G.WOW_PROJECT_ID == _G.WOW_PROJECT_BURNING_CRUSADE_CLASSIC)
local IsClassicWrath = (_G.WOW_PROJECT_ID == _G.WOW_PROJECT_WRATH_CLASSIC)
local IsMainline = (_G.WOW_PROJECT_ID == _G.WOW_PROJECT_MAINLINE)

-- << STATIC CONFIG

local UpdateDelay = .5
local ItemRequestTimeout = 10.0

-- interact distance based checks. ranges are based on my own measurements (thanks for all the folks who helped me with this)
local DefaultInteractList = {
  [3] = 8,
  -- [2] = 9,
  [4] = 28,
}

-- interact list overrides for races
local InteractLists = {
  ["Tauren"] = {
    [3] = 6,
    -- [2] = 7,
    [4] = 25,
  },
  ["Scourge"] = {
    [3] = 7,
    -- [2] = 8,
    [4] = 27,
  },
}

local MeleeRange = 2

-- list of friendly spells that have different ranges
local FriendSpells = {}
-- list of harmful spells that have different ranges
local HarmSpells = {}

FriendSpells["DEATHKNIGHT"] = {
}
HarmSpells["DEATHKNIGHT"] = {
  49576, -- ["Death Grip"], -- 30
}

FriendSpells["DEMONHUNTER"] = {
}
HarmSpells["DEMONHUNTER"] = {
  185123, -- ["Throw Glaive"], -- 30
}

FriendSpells["DRUID"] = {
  774, -- ["Rejuvenation"], -- 40
  2782, -- ["Remove Corruption"], -- 40
}
HarmSpells["DRUID"] = {
  5176, -- ["Wrath"], -- 40
  339, -- ["Entangling Roots"], -- 35
  6795, -- ["Growl"], -- 30
  33786, -- ["Cyclone"], -- 20
  22568, -- ["Ferocious Bite"], -- Melee
}

FriendSpells["HUNTER"] = {}
HarmSpells["HUNTER"] = {
  75, -- ["Auto Shot"], -- 40
}

FriendSpells["MAGE"] = {
}
HarmSpells["MAGE"] = {
  44614, --["Frostfire Bolt"], -- 40
  5019, -- ["Shoot"], -- 30
}

FriendSpells["MONK"] = {
  115450, -- ["Detox"], -- 40
  115546, -- ["Provoke"], -- 30
}
HarmSpells["MONK"] = {
  115546, -- ["Provoke"], -- 30
  115078, -- ["Paralysis"], -- 20
  100780, -- ["Tiger Palm"], -- Melee
}

FriendSpells["PALADIN"] = {
  19750, -- ["Flash of Light"], -- 40
}
HarmSpells["PALADIN"] = {
  62124, -- ["Reckoning"], -- 30
  20271, -- ["Judgement"], -- 30
  853, -- ["Hammer of Justice"], -- 10
  35395, -- ["Crusader Strike"], -- Melee
}

FriendSpells["PRIEST"] = {
  527, -- ["Purify"], -- 40
  17, -- ["Power Word: Shield"], -- 40
}
HarmSpells["PRIEST"] = {
  589, -- ["Shadow Word: Pain"], -- 40
  5019, -- ["Shoot"], -- 30
}

FriendSpells["ROGUE"] = {}
HarmSpells["ROGUE"] = {
  2764, -- ["Throw"], -- 30
  2094, -- ["Blind"], -- 15
}

FriendSpells["SHAMAN"] = {
  8004, -- ["Healing Surge"], -- 40
  546, -- ["Water Walking"], -- 30
}
HarmSpells["SHAMAN"] = {
  403, -- ["Lightning Bolt"], -- 40
  370, -- ["Purge"], -- 30
  73899, -- ["Primal Strike"],. -- Melee
}

FriendSpells["WARRIOR"] = {}
HarmSpells["WARRIOR"] = {
  355, -- ["Taunt"], -- 30
  100, -- ["Charge"], -- 8-25
  5246, -- ["Intimidating Shout"], -- 8
}

FriendSpells["WARLOCK"] = {
  5697, -- ["Unending Breath"], -- 30
}
HarmSpells["WARLOCK"] = {
  686, -- ["Shadow Bolt"], -- 40
  5019, -- ["Shoot"], -- 30
  6789, -- ["Mortal Coil"], -- 20
}

-- Items [Special thanks to Maldivia for the nice list]

local FriendItems  = {
  [1] = {
    90175, -- Gin-Ji Knife Set -- doesn't seem to work for pets (always returns nil)
  },
  [2] = {
    37727, -- Ruby Acorn
  },
  [3] = {
    42732, -- Everfrost Razor
  },
  [4] = {
    129055, -- Shoe Shine Kit
  },
  [5] = {
    8149, -- Voodoo Charm
    136605, -- Solendra's Compassion
    63427, -- Worgsaw
  },
  [6] = {
    164766, -- Iwen's Enchanting Rod
  },
  [7] = {
    61323, -- Ruby Seeds
  },
  [8] = {
    34368, -- Attuned Crystal Cores
    33278, -- Burning Torch
  },
  [10] = {
    32321, -- Sparrowhawk Net
  },
  [15] = {
    1251, -- Linen Bandage
    2581, -- Heavy Linen Bandage
    3530, -- Wool Bandage
    3531, -- Heavy Wool Bandage
    6450, -- Silk Bandage
    6451, -- Heavy Silk Bandage
    8544, -- Mageweave Bandage
    8545, -- Heavy Mageweave Bandage
    14529, -- Runecloth Bandage
    14530, -- Heavy Runecloth Bandage
    21990, -- Netherweave Bandage
    21991, -- Heavy Netherweave Bandage
    34721, -- Frostweave Bandage
    34722, -- Heavy Frostweave Bandage
    -- 38643, -- Thick Frostweave Bandage
    -- 38640, -- Dense Frostweave Bandage
  },
  [20] = {
    21519, -- Mistletoe
  },
  [25] = {
    31463, -- Zezzak's Shard
  },
  [30] = {
    1180, -- Scroll of Stamina
    1478, -- Scroll of Protection II
    3012, -- Scroll of Agility
    1712, -- Scroll of Spirit II
    2290, -- Scroll of Intellect II
    1711, -- Scroll of Stamina II
    34191, -- Handful of Snowflakes
  },
  [35] = {
    18904, -- Zorbin's Ultra-Shrinker
  },
  [38] = {
    140786, -- Ley Spider Eggs
  },
  [40] = {
    34471, -- Vial of the Sunwell
  },
  [45] = {
    32698, -- Wrangling Rope
  },
  [50] = {
    116139, -- Haunting Memento
  },
  [55] = {
    74637, -- Kiryn's Poison Vial
  },
  [60] = {
    32825, -- Soul Cannon
    37887, -- Seeds of Nature's Wrath
  },
  [70] = {
    41265, -- Eyesore Blaster
  },
  [80] = {
    35278, -- Reinforced Net
  },
  [90] = {
    133925, -- Fel Lash
  },
  [100] = {
    41058, -- Hyldnir Harpoon
  },
  [150] = {
    46954, -- Flaming Spears
  },
  [200] = {
    75208, -- Rancher's Lariat
  },
}

local HarmItems = {
  [1] = {
  },
  [2] = {
    37727, -- Ruby Acorn
  },
  [3] = {
    42732, -- Everfrost Razor
  },
  [4] = {
    129055, -- Shoe Shine Kit
  },
  [5] = {
    8149, -- Voodoo Charm
    136605, -- Solendra's Compassion
    63427, -- Worgsaw
  },
  [6] = {
    164766, -- Iwen's Enchanting Rod
  },
  [7] = {
    61323, -- Ruby Seeds
  },
  [8] = {
    34368, -- Attuned Crystal Cores
    33278, -- Burning Torch
  },
  [10] = {
    32321, -- Sparrowhawk Net
  },
  [15] = {
    33069, -- Sturdy Rope
  },
  [20] = {
    10645, -- Gnomish Death Ray
  },
  [25] = {
    24268, -- Netherweave Net
    41509, -- Frostweave Net
    31463, -- Zezzak's Shard
  },
  [30] = {
    835, -- Large Rope Net
    7734, -- Six Demon Bag
    34191, -- Handful of Snowflakes
  },
  [35] = {
    24269, -- Heavy Netherweave Net
    18904, -- Zorbin's Ultra-Shrinker
  },
  [38] = {
    140786, -- Ley Spider Eggs
  },
  [40] = {
    28767, -- The Decapitator
  },
  [45] = {
    -- 32698, -- Wrangling Rope
    23836, -- Goblin Rocket Launcher
  },
  [50] = {
    116139, -- Haunting Memento
  },
  [55] = {
    74637, -- Kiryn's Poison Vial
  },
  [60] = {
    32825, -- Soul Cannon
    37887, -- Seeds of Nature's Wrath
  },
  [70] = {
    41265, -- Eyesore Blaster
  },
  [80] = {
    35278, -- Reinforced Net
  },
  [90] = {
    133925, -- Fel Lash
  },
  [100] = {
    33119, -- Malister's Frost Wand
  },
  [150] = {
    46954, -- Flaming Spears
  },
  [200] = {
    75208, -- Rancher's Lariat
  },
}

-- This could've been done by checking player race as well and creating tables for those, but it's easier like this
for k, v in pairs(FriendSpells) do
  tinsert(v, 28880) -- ["Gift of the Naaru"]
end

-- >> END OF STATIC CONFIG

-- cache

local setmetatable = setmetatable
local tonumber = tonumber
local pairs = pairs
local tostring = tostring
local print = print
local next = next
local type = type
local wipe = wipe
local tinsert = tinsert
local tremove = tremove
local BOOKTYPE_SPELL = BOOKTYPE_SPELL
local GetSpellInfo = GetSpellInfo
local GetSpellBookItemName = GetSpellBookItemName
local GetNumSpellTabs = GetNumSpellTabs
local GetSpellTabInfo = GetSpellTabInfo
local GetItemInfo = GetItemInfo
local UnitAura = UnitAura
local UnitCanAttack = UnitCanAttack
local UnitCanAssist = UnitCanAssist
local UnitExists = UnitExists
local UnitIsDeadOrGhost = UnitIsDeadOrGhost
local CheckInteractDistance = CheckInteractDistance
local IsSpellInRange = IsSpellInRange
local IsItemInRange = IsItemInRange
local UnitClass = UnitClass
local UnitRace = UnitRace
local GetInventoryItemLink = GetInventoryItemLink
local GetSpecialization = GetSpecialization
local GetSpecializationInfo = GetSpecializationInfo
local GetTime = GetTime
local HandSlotId = GetInventorySlotInfo("HandsSlot")
local math_floor = math.floor
local UnitIsVisible = UnitIsVisible

-- temporary stuff

local pendingItemRequest
local itemRequestTimeoutAt
local foundNewItems
local cacheAllItems
local friendItemRequests
local harmItemRequests
local lastUpdate = 0

local function null() end
local useCombatCheckers = IsMainline and InCombatLockdown or null

local checkers_Spell = setmetatable({}, {
  __index = function(t, spellIdx)
    local func = function(unit)
      if IsSpellInRange(spellIdx, BOOKTYPE_SPELL, unit) == 1 then
         return true
      end
    end
    t[spellIdx] = func
    return func
  end
})
local checkers_SpellWithMin = {} -- see getCheckerForSpellWithMinRange()
local checkers_Item = setmetatable({}, {
  __index = function(t, item)
    local func = function(unit)
      return IsItemInRange(item, unit)
    end
    t[item] = func
    return func
  end
})
local checkers_Interact = setmetatable({}, {
  __index = function(t, index)
    local func = function(unit)
      if CheckInteractDistance(unit, index) then
         return true
      end
    end
    t[index] = func
    return func
  end
})

-- helper functions

local function copyTable(src, dst)
  if type(dst) ~= "table" then dst = {} end
  if type(src) == "table" then
    for k, v in pairs(src) do
      if type(v) == "table" then
        v = copyTable(v, dst[k])
      end
      dst[k] = v
    end
  end
  return dst
end


local function initItemRequests(cacheAll)
  friendItemRequests = copyTable(FriendItems)
  harmItemRequests = copyTable(HarmItems)
  cacheAllItems = cacheAll
  foundNewItems = nil
end

local function getNumSpells()
  local _, _, offset, numSpells = GetSpellTabInfo(GetNumSpellTabs())
  return offset + numSpells
end

-- return the spellIndex of the given spell by scanning the spellbook
local function findSpellIdx(spellName)
  if not spellName or spellName == "" then
    return nil
  end
  for i = 1, getNumSpells() do
    local spell, rank = GetSpellBookItemName(i, BOOKTYPE_SPELL)
    if spell == spellName then return i end
  end
  return nil
end

-- minRange should be nil if there's no minRange, not 0
local function addChecker(t, range, minRange, checker, info)
  local rc = { ["range"] = range, ["minRange"] = minRange, ["checker"] = checker, ["info"] = info }
  for i = 1, #t do
    local v = t[i]
    if rc.range == v.range then return end
    if rc.range > v.range then
      tinsert(t, i, rc)
      return
    end
  end
  tinsert(t, rc)
end

local function fixRange(range)
  if range then
    return math_floor(range + 0.5)
  end
end

local function getSpellData(sid)
  local name, _, _, _, minRange, range = GetSpellInfo(sid)
  return name, fixRange(minRange), fixRange(range), findSpellIdx(name)
end

local function findMinRangeChecker(origMinRange, origRange, spellList)
  for i = 1, #spellList do
    local sid = spellList[i]
    local name, minRange, range, spellIdx = getSpellData(sid)
    -- print("### checking minChecker: " .. tostring(name) .. ", idx: " .. tostring(spellIdx) .. ", " .. tostring(minRange) .. " - " ..  tostring(range) .. " for range " .. origMinRange .. " - " .. origRange)
    if range and spellIdx and origMinRange <= range and range <= origRange and minRange == 0 then
      -- print("### using minChecker: " .. tostring(name) .. ", " .. tostring(minRange) .. " - " ..  tostring(range) .. " for range " .. origMinRange .. " - " .. origRange)
      return checkers_Spell[findSpellIdx(name)]
    end
  end
end

local function getCheckerForSpellWithMinRange(spellIdx, minRange, range, spellList)
  local checker = checkers_SpellWithMin[spellIdx]
  if checker then
    return checker
  end
  local minRangeChecker = findMinRangeChecker(minRange, range, spellList)
  if minRangeChecker then
    checker = function(unit)
      if IsSpellInRange(spellIdx, BOOKTYPE_SPELL, unit) == 1 then
        return true
      elseif minRangeChecker(unit) then
        return true, true
      end
    end
    checkers_SpellWithMin[spellIdx] = checker
    return checker
  end
end

local function createCheckerList(spellList, itemList, interactList)
  local res = {}
  if itemList then
    for range, items in pairs(itemList) do
      for i = 1, #items do
        local item = items[i]
        if GetItemInfo(item) then
          addChecker(res, range, nil, checkers_Item[item], "item:" .. item)
          break
        end
      end
    end
  end

  if spellList then
    for i = 1, #spellList do
      local sid = spellList[i]
      local name, minRange, range, spellIdx = getSpellData(sid)
      if spellIdx and range then
        -- print("### spell: " .. tostring(name) .. ", " .. tostring(minRange) .. " - " ..  tostring(range))
        if minRange == 0 then -- getRange() expects minRange to be nil in this case
          minRange = nil
        end
        if range == 0 then
          range = MeleeRange
        end
        if minRange then
          local checker = getCheckerForSpellWithMinRange(spellIdx, minRange, range, spellList)
          if checker then
            addChecker(res, range, minRange, checker, "spell:" .. sid .. ":" .. tostring(name))
          end
        else
          addChecker(res, range, minRange, checkers_Spell[spellIdx], "spell:" .. sid .. ":" .. tostring(name))
        end
      end
    end
  end

  if interactList and not next(res) then
    for index, range in pairs(interactList) do
      addChecker(res, range, nil,  checkers_Interact[index], "interact:" .. index)
    end
  end

  return res
end

-- returns minRange, maxRange  or nil
local function getRange(unit, checkerList)
  local lo, hi = 1, #checkerList
  if hi == 0 then
    return nil
  end
  while lo <= hi do
    local mid = math_floor((lo + hi) / 2)
    local rc = checkerList[mid]
    if rc.checker(unit) then
      lo = mid + 1
    else
      hi = mid - 1
    end
  end
  if lo > #checkerList then
    return 0, checkerList[#checkerList].range
  elseif lo <= 1 then
    return checkerList[1].range, nil
  else
    return checkerList[lo].range, checkerList[lo - 1].range
  end
end

local function updateCheckers(origList, newList)
  if #origList ~= #newList then
    wipe(origList)
    copyTable(newList, origList)
    return true
  end
  for i = 1, #origList do
    if origList[i].range ~= newList[i].range or origList[i].checker ~= newList[i].checker then
      wipe(origList)
      copyTable(newList, origList)
      return true
    end
  end
end

local function rcIterator(checkerList)
  local curr = #checkerList
  return function()
    local rc = checkerList[curr]
    if not rc then
       return nil
    end
    curr = curr - 1
    return rc.range, rc.checker
  end
end

local function getMinChecker(checkerList, range)
  local checker, checkerRange
  for i = 1, #checkerList do
    local rc = checkerList[i]
    if rc.range < range then
      return checker, checkerRange
    end
    checker, checkerRange = rc.checker, rc.range
  end
  return checker, checkerRange
end

local function getMaxChecker(checkerList, range)
  for i = 1, #checkerList do
    local rc = checkerList[i]
    if rc.range <= range then
      return rc.checker, rc.range
    end
  end
end

local function getChecker(checkerList, range)
  for i = 1, #checkerList do
    local rc = checkerList[i]
    if rc.range == range then
      return rc.checker
    end
  end
end

local function createSmartChecker(friendChecker, harmChecker, miscChecker)
  miscChecker = miscChecker or null
  friendChecker = friendChecker or miscChecker
  harmChecker = harmChecker or miscChecker
  return function(unit)
    if not UnitExists(unit) then
      return nil
    end
    if UnitIsDeadOrGhost(unit) then
      return miscChecker(unit)
    end
    if UnitCanAttack("player", unit) then
      return harmChecker(unit)
    elseif UnitCanAssist("player", unit) then
      return friendChecker(unit)
    else
      return miscChecker(unit)
    end
  end
end

-- OK, here comes the actual lib

-- pre-initialize the checkerLists here so that we can return some meaningful result even if
-- someone manages to call us before we're properly initialized. miscRC should be independent of
-- race/class/talents, so it's safe to initialize it here
-- friendRC and harmRC will be properly initialized later when we have all the necessary data for them
lib.checkerCache_Spell = lib.checkerCache_Spell or {}
lib.checkerCache_Item = lib.checkerCache_Item or {}

lib.miscRC = createCheckerList(nil, nil, DefaultInteractList)
lib.friendRC = createCheckerList(nil, nil, DefaultInteractList)
lib.harmRC = createCheckerList(nil, nil, DefaultInteractList)

lib.miscRCInCombat = createCheckerList(nil, nil, nil)
lib.friendRCInCombat = createCheckerList(nil, nil, nil)
lib.harmRCInCombat = createCheckerList(nil, nil, nil)

lib.failedItemRequests = {}

-- << Public API


--- The callback name that is fired when checkers are changed.
-- @field
lib.CHECKERS_CHANGED = "CHECKERS_CHANGED"
-- "export" it, maybe someone will need it for formatting
--- Constant for Melee range (2yd).
-- @field
lib.MeleeRange = MeleeRange

function lib:findSpellIndex(spell)
  if type(spell) == 'number' then
    spell = GetSpellInfo(spell)
  end
  return findSpellIdx(spell)
end

-- returns the range estimate as a string
-- deprecated, use :getRange(unit) instead and build your own strings
-- @param checkVisible if set to true, then a UnitIsVisible check is made, and **nil** is returned if the unit is not visible
function lib:getRangeAsString(unit, checkVisible, showOutOfRange)
  local minRange, maxRange = self:getRange(unit, checkVisible)
  if not minRange then return nil end
  if not maxRange then
    return showOutOfRange and minRange .. " +" or nil
  end
  return minRange .. " - " .. maxRange
end

-- initialize RangeCheck if not yet initialized or if "forced"
function lib:init(forced)
  if self.initialized and (not forced) then
    return
  end
  self.initialized = true
  local _, playerClass = UnitClass("player")
  local _, playerRace = UnitRace("player")

  local interactList = InteractLists[playerRace] or DefaultInteractList
  self.handSlotItem = GetInventoryItemLink("player", HandSlotId)

  local changed = false
  changed = updateCheckers(self.friendRC, createCheckerList(FriendSpells[playerClass], FriendItems, interactList)) or changed
  changed = updateCheckers(self.friendRCInCombat, createCheckerList(FriendSpells[playerClass], nil, nil)) or changed
  changed = updateCheckers(self.harmRC, createCheckerList(HarmSpells[playerClass], HarmItems, interactList)) or changed
  changed = updateCheckers(self.harmRCInCombat, createCheckerList(HarmSpells[playerClass], nil, nil)) or changed
  changed = updateCheckers(self.miscRC, createCheckerList(nil, nil, interactList)) or changed
  changed = updateCheckers(self.miscRCInCombat, createCheckerList(nil, nil, nil)) or changed

  if changed and self.callbacks then
    self.callbacks:Fire(self.CHECKERS_CHANGED)
  end
end

--- Return an iterator for checkers usable on friendly units as (**range**, **checker**) pairs.
function lib:GetFriendCheckers()
  return rcIterator(self.friendRC)
end

--- Return an iterator for checkers usable on enemy units as (**range**, **checker**) pairs.
function lib:GetHarmCheckers()
  return rcIterator(self.harmRC)
end

--- Return an iterator for checkers usable on miscellaneous units as (**range**, **checker**) pairs.  These units are neither enemy nor friendly, such as people in sanctuaries or corpses.
function lib:GetMiscCheckers()
  return rcIterator(self.miscRC)
end

--- Return a checker suitable for out-of-range checking on friendly units, that is, a checker whose range is equal or larger than the requested range.
-- @param range the range to check for.
-- @return **checker**, **range** pair or **nil** if no suitable checker is available. **range** is the actual range the returned **checker** checks for.
function lib:GetFriendMinChecker(range)
  return getMinChecker(self.friendRC, range)
end

--- Return a checker suitable for out-of-range checking on enemy units, that is, a checker whose range is equal or larger than the requested range.
-- @param range the range to check for.
-- @return **checker**, **range** pair or **nil** if no suitable checker is available. **range** is the actual range the returned **checker** checks for.
function lib:GetHarmMinChecker(range)
  return getMinChecker(self.harmRC, range)
end

--- Return a checker suitable for out-of-range checking on miscellaneous units, that is, a checker whose range is equal or larger than the requested range.
-- @param range the range to check for.
-- @return **checker**, **range** pair or **nil** if no suitable checker is available. **range** is the actual range the returned **checker** checks for.
function lib:GetMiscMinChecker(range)
  return getMinChecker(self.miscRC, range)
end

--- Return a checker suitable for in-range checking on friendly units, that is, a checker whose range is equal or smaller than the requested range.
-- @param range the range to check for.
-- @return **checker**, **range** pair or **nil** if no suitable checker is available. **range** is the actual range the returned **checker** checks for.
function lib:GetFriendMaxChecker(range)
  return getMaxChecker(self.friendRC, range)
end

--- Return a checker suitable for in-range checking on enemy units, that is, a checker whose range is equal or smaller than the requested range.
-- @param range the range to check for.
-- @return **checker**, **range** pair or **nil** if no suitable checker is available. **range** is the actual range the returned **checker** checks for.
function lib:GetHarmMaxChecker(range)
  return getMaxChecker(self.harmRC, range)
end

--- Return a checker suitable for in-range checking on miscellaneous units, that is, a checker whose range is equal or smaller than the requested range.
-- @param range the range to check for.
-- @return **checker**, **range** pair or **nil** if no suitable checker is available. **range** is the actual range the returned **checker** checks for.
function lib:GetMiscMaxChecker(range)
  return getMaxChecker(self.miscRC, range)
end

--- Return a checker for the given range for friendly units.
-- @param range the range to check for.
-- @return **checker** function or **nil** if no suitable checker is available.
function lib:GetFriendChecker(range)
  return getChecker(self.friendRC, range)
end

--- Return a checker for the given range for enemy units.
-- @param range the range to check for.
-- @return **checker** function or **nil** if no suitable checker is available.
function lib:GetHarmChecker(range)
  return getChecker(self.harmRC, range)
end

--- Return a checker for the given range for miscellaneous units.
-- @param range the range to check for.
-- @return **checker** function or **nil** if no suitable checker is available.
function lib:GetMiscChecker(range)
  return getChecker(self.miscRC, range)
end

--- Return a checker suitable for out-of-range checking that checks the unit type and calls the appropriate checker (friend/harm/misc).
-- @param range the range to check for.
-- @return **checker** function.
function lib:GetSmartMinChecker(range)
  return createSmartChecker(
    getMinChecker(self.friendRC, range),
    getMinChecker(self.harmRC, range),
    getMinChecker(self.miscRC, range))
end

--- Return a checker suitable for in-range checking that checks the unit type and calls the appropriate checker (friend/harm/misc).
-- @param range the range to check for.
-- @return **checker** function.
function lib:GetSmartMaxChecker(range)
  return createSmartChecker(
    getMaxChecker(self.friendRC, range),
    getMaxChecker(self.harmRC, range),
    getMaxChecker(self.miscRC, range))
end

--- Return a checker for the given range that checks the unit type and calls the appropriate checker (friend/harm/misc).
-- @param range the range to check for.
-- @param fallback optional fallback function that gets called as fallback(unit) if a checker is not available for the given type (friend/harm/misc) at the requested range. The default fallback function return nil.
-- @return **checker** function.
function lib:GetSmartChecker(range, fallback)
  return createSmartChecker(
    getChecker(self.friendRC, range) or fallback,
    getChecker(self.harmRC, range) or fallback,
    getChecker(self.miscRC, range) or fallback)
end

--- Get a range estimate as **minRange**, **maxRange**.
-- @param unit the target unit to check range to.
-- @param checkVisible if set to true, then a UnitIsVisible check is made, and **nil** is returned if the unit is not visible
-- @return **minRange**, **maxRange** pair if a range estimate could be determined, **nil** otherwise. **maxRange** is **nil** if **unit** is further away than the highest possible range we can check.
-- Includes checks for unit validity and friendly/enemy status.
-- @usage
-- local rc = LibStub("LibRangeCheck-2.0")
-- local minRange, maxRange = rc:GetRange('target')
-- local minRangeIfVisible, maxRangeIfVisible = rc:GetRange('target', true)
function lib:GetRange(unit, checkVisible)
  if not UnitExists(unit) then
    return nil
  end
  if checkVisible and not UnitIsVisible(unit) then
    return nil
  end
  if UnitIsDeadOrGhost(unit) then
    return getRange(unit, useCombatCheckers() and self.miscRCInCombat or self.miscRC)
  end
  if UnitCanAttack("player", unit) then
    return getRange(unit, useCombatCheckers() and self.harmRCInCombat or self.harmRC)
  elseif UnitCanAssist("player", unit) then
    return getRange(unit, useCombatCheckers() and self.friendRCInCombat or self.friendRC)
  else
    return getRange(unit, useCombatCheckers() and self.miscRCInCombat or self.miscRC)
  end
end

-- keep this for compatibility
lib.getRange = lib.GetRange

-- >> Public API

function lib:OnEvent(event, ...)
  if type(self[event]) == 'function' then
    self[event](self, event, ...)
  end
end

function lib:LEARNED_SPELL_IN_TAB()
  self:scheduleInit()
end

function lib:CHARACTER_POINTS_CHANGED()
  self:scheduleInit()
end

function lib:PLAYER_TALENT_UPDATE()
  self:scheduleInit()
end

function lib:SPELLS_CHANGED()
  self:scheduleInit()
end

function lib:UNIT_INVENTORY_CHANGED(event, unit)
  if self.initialized and unit == "player" and self.handSlotItem ~= GetInventoryItemLink("player", HandSlotId) then
    self:scheduleInit()
  end
end

function lib:UNIT_AURA(event, unit)
  if self.initialized and unit == "player" then
    self:scheduleAuraCheck()
  end
end

function lib:GET_ITEM_INFO_RECEIVED(event, item, success)
  -- print("### GET_ITEM_INFO_RECEIVED: " .. tostring(item) .. ", " .. tostring(success))
  if item == pendingItemRequest then
    pendingItemRequest = nil
    if not success then
      self.failedItemRequests[item] = true
    end
    lastUpdate = UpdateDelay
  end
end

function lib:processItemRequests(itemRequests)
  while true do
    local range, items = next(itemRequests)
    if not range then return end
    while true do
      local i, item = next(items)
      if not i then
        itemRequests[range] = nil
        break
      elseif self.failedItemRequests[item] then
        -- print("### processItemRequests: failed: " .. tostring(item))
        tremove(items, i)
      elseif item == pendingItemRequest and GetTime() < itemRequestTimeoutAt then
        return true; -- still waiting for server response
      elseif GetItemInfo(item) then
        -- print("### processItemRequests: found: " .. tostring(item))
        if itemRequestTimeoutAt then
          -- print("### processItemRequests: new: " .. tostring(item))
          foundNewItems = true
          itemRequestTimeoutAt = nil
          pendingItemRequest = nil
        end
        if not cacheAllItems then
          itemRequests[range] = nil
          break
        end
        tremove(items, i)
      elseif not itemRequestTimeoutAt then
        -- print("### processItemRequests: waiting: " .. tostring(item))
        itemRequestTimeoutAt = GetTime() + ItemRequestTimeout
        pendingItemRequest = item
        if not self.frame:IsEventRegistered("GET_ITEM_INFO_RECEIVED") then
          self.frame:RegisterEvent("GET_ITEM_INFO_RECEIVED")
        end
        return true
      elseif GetTime() >= itemRequestTimeoutAt then
        -- print("### processItemRequests: timeout: " .. tostring(item))
        if cacheAllItems then
          print(MAJOR_VERSION .. ": timeout for item: " .. tostring(item))
        end
        self.failedItemRequests[item] = true
        itemRequestTimeoutAt = nil
        pendingItemRequest = nil
        tremove(items, i)
      else
        return true -- still waiting for server response
      end
    end
  end
end

function lib:initialOnUpdate()
  self:init()
  if friendItemRequests then
    if self:processItemRequests(friendItemRequests) then return end
    friendItemRequests = nil
  end
  if harmItemRequests then
    if self:processItemRequests(harmItemRequests) then return end
    harmItemRequests = nil
  end
  if foundNewItems then
    self:init(true)
    foundNewItems = nil
  end
  if cacheAllItems then
    print(MAJOR_VERSION .. ": finished cache")
    cacheAllItems = nil
  end
  self.frame:Hide()
  self.frame:UnregisterEvent("GET_ITEM_INFO_RECEIVED")
end

function lib:scheduleInit()
  self.initialized = nil
  lastUpdate = 0
  self.frame:Show()
end

function lib:scheduleAuraCheck()
  lastUpdate = UpdateDelay
  self.frame:Show()
end


-- << load-time initialization

function lib:activate()
  if not self.frame then
    local frame = CreateFrame("Frame")
    self.frame = frame
    frame:RegisterEvent("LEARNED_SPELL_IN_TAB")
    frame:RegisterEvent("CHARACTER_POINTS_CHANGED")
    if IsMainline or IsClassicWrath then
      frame:RegisterEvent("PLAYER_TALENT_UPDATE")
    end
    frame:RegisterEvent("SPELLS_CHANGED")
    local _, playerClass = UnitClass("player")
    if playerClass == "MAGE" or playerClass == "SHAMAN" then
      -- Mage and Shaman gladiator gloves modify spell ranges
      frame:RegisterUnitEvent("UNIT_INVENTORY_CHANGED", "player")
    end
  end
  initItemRequests()
  self.frame:SetScript("OnEvent", function(frame, ...) self:OnEvent(...) end)
  self.frame:SetScript("OnUpdate", function(frame, elapsed)
    lastUpdate = lastUpdate + elapsed
    if lastUpdate < UpdateDelay then
      return
    end
    lastUpdate = 0
    self:initialOnUpdate()
  end)
  self:scheduleInit()
end

--- BEGIN CallbackHandler stuff

do
  local lib = lib -- to keep a ref even though later we nil lib
  --- Register a callback to get called when checkers are updated
  -- @class function
  -- @name lib.RegisterCallback
  -- @usage
  -- rc.RegisterCallback(self, rc.CHECKERS_CHANGED, "myCallback")
  -- -- or
  -- rc.RegisterCallback(self, "CHECKERS_CHANGED", someCallbackFunction)
  -- @see CallbackHandler-1.0 documentation for more details
  lib.RegisterCallback = lib.RegisterCallback or function(...)
    local CBH = LibStub("CallbackHandler-1.0")
    lib.RegisterCallback = nil -- extra safety, we shouldn't get this far if CBH is not found, but better an error later than an infinite recursion now
    lib.callbacks = CBH:New(lib)
    -- ok, CBH hopefully injected or new shiny RegisterCallback
    return lib.RegisterCallback(...)
  end
end

--- END CallbackHandler stuff

lib:activate()
lib = nil
